package simulator;

import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.ListIterator;
import java.util.Map.Entry;

import jsonreader.JSONParseException;

public class Simulator {
	
	/**
	 * Increments time by one second and performs all actions relevent to this event
	 * Such as finish building units
	 * @param previous - GameState to be incremented
	 */
	public void tickState (GameState previous) {
		// Increment time
		previous.time++;
		
		BuildFinishEvent event = null;
		ListIterator<BuildFinishEvent> it = previous.pendingEvents.listIterator();
		
		// Whilst there are events which are in the pendingEvents list
		while(it.hasNext()) {
			event = it.next();
			
			// If they finish at the current time...
			if(event.getEndTime() == previous.time) {
				GameObject obj = GameDataStore.getGameObjectFromGameAsset(event.getGameAsset());
				
				// add the unit to the GameState
				previous.addUnit(event.getGameAsset());
				
				// if the supply is negative, it's a game object which adds to supply. This has to be applied after it's completed being built.
				if(obj.supply < 0) {
					previous.assetNumbers.put(GameAsset.SUPPLY, previous.assetNumbers.get(GameAsset.SUPPLY) - obj.supply);
				}
				
				// mark the build slots used for this item as free
				for(Entry<GameAsset, Integer> blockedAsset : obj.blockingDependencies.entrySet()) {
					previous.assetFreeSlots.put(blockedAsset.getKey(), previous.assetFreeSlots.get(blockedAsset.getKey()) + blockedAsset.getValue());
				}
				
				// if this used up a worker, remove it from the building assignment
				if(obj.occupyWorkers)
				{
					// take one worker away from the building assignment
					previous.workerAssignments.put(WorkerState.BUILDING, previous.workerAssignments.get(WorkerState.BUILDING) - 1);
					
					previous.workerAssignments.put(previous.defaultWorkerAssignment, previous.workerAssignments.get(previous.defaultWorkerAssignment) + 1);
				}
				
				// if the object built has build slots, add them to the free slots
				previous.assetFreeSlots.put(event.getGameAsset(), previous.assetFreeSlots.get(event.getGameAsset()) + obj.buildSlots);
				try {
					it.remove();
				}
				catch(Exception e) {
					System.out.println("Encountered Exception " + e.getMessage());
				}
			}
		}
		
		// Calculate new resources generated in this turn
		double minerals = previous.getNumberOfAsset(GameAsset.MINERAL) + previous.mineralAdjustment;
		minerals = calculateNewResourcesGenerated(WorkerState.GET_MINERALS, minerals, previous);
		
		int wholeMinerals = (int)Math.floor(minerals);
		previous.assetNumbers.put(GameAsset.MINERAL, wholeMinerals);
		previous.mineralAdjustment = minerals - wholeMinerals;
		
		
		double vespene = previous.getNumberOfAsset(GameAsset.VESPENE_GAS) + previous.gasAdjustment; 
		vespene = calculateNewResourcesGenerated(WorkerState.GET_VESPENE, vespene, previous);
		
		int wholeVespene = (int)Math.floor(vespene);
		previous.assetNumbers.put(GameAsset.VESPENE_GAS, wholeVespene);
		previous.gasAdjustment = vespene - wholeVespene;
		
		//Energy could be calculated here
	}
	
	/**
	 * Encapsulates all logic regarding calculating new resources for the turn
	 * Does not do any rounding
	 * @param workerState
	 * @param oldValue
	 * @param state
	 * @return returns the exact number of resources generated that turn
	 */
	private double calculateNewResourcesGenerated(WorkerState workerState, double oldValue, GameState state) {
		int numWorkingWorkers = state.getNumWorkersInState(workerState);
		
		int numMostProductive = 0, numTotalProductive = 0;
		double mostProductiveGain = 0, otherGain = 0;
		
		if(workerState == WorkerState.GET_MINERALS) {
			numMostProductive = 2 * state.assetNumbers.get(GameAsset.MINERAL_PATCH);
			numTotalProductive = 3 * state.assetNumbers.get(GameAsset.MINERAL_PATCH);
			
			mostProductiveGain = 41.0/60.0;
			otherGain = 20.0/60.0;
		}
		else {
			// We only set the less productive variables since gas workers collect the same if there is a spot for them
			numTotalProductive = state.assetNumbers.get(GameAsset.REFINERY);
			
			otherGain = 38.0/40.0;
		}
		
		double newResources = oldValue;
		
		for(int i=0; i<numWorkingWorkers; i++) {
			
			if(i < numMostProductive) {
				newResources += mostProductiveGain;
			}
			else if(i < numTotalProductive) {
				newResources += otherGain;
			}
			else {
				// Any more workers are redundant
				break;
			}
		}
		return newResources;
	}
	
	/**
	 * Returns an arraylist of all available choices for the GameState passed in
	 * This list includes GameMove.WAIT
	 * @param previous - the GameState to be tested
	 * @return list of choices
	 */
	public ArrayList<GameChoice> getAllAvailableChoices (GameState previous) {
		ArrayList<GameChoice> choices = new ArrayList<GameChoice>();
		
		// Add the Do nothing option. In theory this shouldn't be needed, but it makes it crystal
		// clear that this is an option
		choices.add(new GameChoice(GameMove.WAIT, null));
		
		// iterate over all game assets to see if they can be built
		for(GameAsset asset : GameAsset.values()) {
			if(this.canBuild(previous, asset)) {
				choices.add(new GameChoice(GameMove.BUILD, asset));
			}
		}
		
		return choices;
	}
	
	/**
	 * Performs a choice. The choice performed must be valid, we do no checking here.
	 * This returns a new GameState object cloned from the passed GameState
	 * @param state
	 * @param choice
	 * @return a new GameState object which has the choice performed
	 */
	public GameState choose(GameState state, GameChoice choice) {
		// In reality, choices must be valid, unless there is an error in the simulator package,
		// because the constructor for GameChoice is protected

		GameState newState = new GameState(state);

		if(choice.getTask() == GameMove.BUILD) {
			GameObject obj = GameDataStore.getGameObjectFromGameAsset(choice.getGameAsset());
			
			newState.assetNumbers.put(GameAsset.MINERAL, newState.assetNumbers.get(GameAsset.MINERAL) - obj.mineralCost);
			newState.assetNumbers.put(GameAsset.VESPENE_GAS, newState.assetNumbers.get(GameAsset.VESPENE_GAS) - obj.vespeneCost);
			// Handles only objects which use up supply, not add to
			if(obj.supply > 0) {
				newState.assetNumbers.put(GameAsset.SUPPLY, newState.assetNumbers.get(GameAsset.SUPPLY) - obj.supply);
			}
			
			// marked blocking dependency slots as not available
			for(Entry<GameAsset, Integer> blocking : obj.blockingDependencies.entrySet()) {
				newState.assetFreeSlots.put(blocking.getKey(), newState.assetFreeSlots.get(blocking.getKey()) - blocking.getValue());
			}
			
			int numWorkersMinerals = newState.getNumWorkersInState(WorkerState.GET_MINERALS);
			int numWorkersGas = newState.getNumWorkersInState(WorkerState.GET_VESPENE);
			
			if(obj.occupyWorkers)
			{
				// Take a worker out of doing what it's currently doing.
				if(numWorkersGas >= numWorkersMinerals) {
					numWorkersGas--;
					newState.workerAssignments.put(WorkerState.BUILDING, newState.workerAssignments.get(WorkerState.BUILDING) + 1);
					newState.setWorkerDefaultTask(WorkerState.GET_VESPENE);
				}
				else {
					numWorkersMinerals--;
					newState.workerAssignments.put(WorkerState.BUILDING, newState.workerAssignments.get(WorkerState.BUILDING) + 1);
					newState.setWorkerDefaultTask(WorkerState.GET_MINERALS);
				}
				newState.setWorkersTask(numWorkersMinerals, numWorkersGas);
			}
			
			newState.pendingEvents.add(new BuildFinishEvent(newState.time, choice.getGameAsset()));
		}
		else if(choice.getTask() == GameMove.WAIT) {
		}
		else {
			throw new Error("Unexpected GameChoice value " + choice.getTask());
		}
		return newState;
	}
	
	/**
	 * 
	 * @param file
	 * @throws FileNotFoundException
	 * @throws JSONParseException
	 * @throws StarcraftDataParseException
	 */
	public void loadFromJSON(String file) throws FileNotFoundException, JSONParseException, StarcraftDataParseException {
		GameDataStore.loadDataFromJSON(file);
	}
	
	/**
	 * Can the current GameState possibly build the GameAsset at this point in time?
	 * @param GameState
	 * @param GameAsset
	 * @return true or false to the above question 
	 */
	private boolean canBuild(GameState state, GameAsset asset) {
		GameObject obj = GameDataStore.getGameObjectFromGameAsset(asset);
		if(obj == null) {
			// asset is not implemented in the JSON file
			return false;
		}
		
		// Do we have enough minerals?
		if(obj.mineralCost > state.assetNumbers.get(GameAsset.MINERAL)) {
			return false;
		}
		// Do we have enough gas?
		if(obj.vespeneCost > state.assetNumbers.get(GameAsset.VESPENE_GAS)) {
			return false;
		}
		// Do we have enough supply?
		if(obj.supply > 0 && obj.supply > state.assetNumbers.get(GameAsset.SUPPLY)) {
			return false;
		}
		
		if(asset == GameAsset.REFINERY)
		{
			if(state.getTotalNumberOfAssets(GameAsset.REFINERY) >= state.getTotalNumberOfAssets(GameAsset.VESPENE_GEYSER))
			{
				return false;
			}
		}
		
		for(Entry<GameAsset, Integer> dependentAsset : obj.dependencies.entrySet()) {
			
			// Do we have enough of the dependent GameAsset to build?
			// (System designed so that some assets could require multiple of each dependency)
			if(dependentAsset.getValue() > state.assetNumbers.get(dependentAsset.getKey())) {
				return false;
			}
		}
		
		for(Entry<GameAsset, Integer> blockingAsset : obj.blockingDependencies.entrySet()) {
			
			// Do we have enough of the blocking GameAsset to build?
			// (System designed so that some assets could block multiple slots)
			if(blockingAsset.getValue() > state.assetFreeSlots.get(blockingAsset.getKey())) {
				return false;
			}
		}
		
		// Do we have any workers free to build this?
		if(obj.occupyWorkers)
		{
			if(state.getNumberOfAsset(GameAsset.WORKER) - state.getNumWorkersInState(WorkerState.BUILDING) < 1)
			{
				return false;
			}
		}
		
		// If we got this far, we must be able to build 'asset'
		return true;
	}
}
